
import sounddevice as sd
import soundfile as sf
import time
import logging
import os
from datetime import datetime
import threading
import traceback
import sys
import argparse
import pystray
from PIL import Image
import signal
import atexit

# 全局播放控制标志
is_playing = False
play_thread = None
current_log_path = None
# 退出标志
exit_flag = False

# 信号处理函数
def signal_handler(signum, frame):
    global is_playing, exit_flag
    exit_flag = True
    is_playing = False
    # 获取logger实例
    logger = logging.getLogger()
    logger.info(f"收到信号 {signum}，准备退出...")
    print("\n收到停止信号，正在停止播放...\n")

# 定义颜色代码
GREEN = '\033[92m'
END = '\033[0m'

def setup_logging():
    """配置INFO日志"""
    base_log_dir = "BellPlayerLog"
    os.makedirs(base_log_dir, exist_ok=True)
    
    now = datetime.now()
    info_date_dir = os.path.join(base_log_dir, "INFO", now.strftime("%Y"), now.strftime("%m"), now.strftime("%d"))
    os.makedirs(info_date_dir, exist_ok=True)
    
    timestamp = now.strftime("%Y%m%d_%H-%M-%S_%f")
    info_log = os.path.join(info_date_dir, f"{timestamp}.log")
    
    logger = logging.getLogger()
    logger.setLevel(logging.INFO)
    
    # INFO处理器
    info_handler = logging.FileHandler(info_log, encoding='utf-8')
    info_formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
    info_handler.setFormatter(info_formatter)
    logger.addHandler(info_handler)
    
    # 控制台处理器
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.INFO)
    console_formatter = logging.Formatter('%(message)s')
    console_handler.setFormatter(console_formatter)
    logger.addHandler(console_handler)
    
    return logger, info_log

def create_level_log(logger, level, message, trace=None):
    """创建WARNING或ERROR日志"""
    base_log_dir = "log"
    dir_name = "WARNING" if level == logging.WARNING else "ERROR"
    dir_path = os.path.join(base_log_dir, dir_name)
    os.makedirs(dir_path, exist_ok=True)
    
    timestamp = datetime.now().strftime("%Y%m%d_%H-%M-%S_%f")
    log_file = os.path.join(dir_path, f"{timestamp}.log")
    
    handler = logging.FileHandler(log_file, encoding='utf-8')
    formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
    handler.setFormatter(formatter)
    logger.addHandler(handler)
    
    if level == logging.WARNING:
        logger.warning(message)
    else:
        logger.error(message)
        if trace:
            logger.error(trace)
    
    logger.removeHandler(handler)

def validate_audio_file(file_path):
    """验证音频文件是否存在且可读"""
    if not os.path.exists(file_path):
        raise FileNotFoundError(f"文件不存在: {file_path}")
    
    if not os.path.isfile(file_path):
        raise ValueError(f"不是有效的文件: {file_path}")
    
    if not os.access(file_path, os.R_OK):
        raise PermissionError(f"文件不可读: {file_path}")
    
    # 检查文件扩展名是否为支持的格式
    supported_formats = ['.wav', '.flac', '.ogg', '.mp3']
    file_ext = os.path.splitext(file_path)[1].lower()
    if file_ext not in supported_formats:
        raise ValueError(f"不支持的文件格式: {file_ext}，支持的格式: {', '.join(supported_formats)}")

def check_audio_device(device_id=None):
    """检查音频设备是否可用"""
    if device_id is None:
        device_id = sd.default.device[1]  # 默认输出设备
    
    devices = sd.query_devices()
    if device_id >= len(devices):
        raise ValueError(f"设备ID {device_id} 不存在")
    
    device_info = devices[device_id]
    if not device_info['max_output_channels'] > 0:
        raise ValueError(f"设备 {device_id} ({device_info['name']}) 不是有效的输出设备")
    
    return device_info

def open_latest_log(icon, item):
    """打开最新的日志文件"""
    global current_log_path
    if current_log_path and os.path.exists(current_log_path):
        try:
            # 在Windows上使用默认程序打开文件
            os.startfile(current_log_path)
        except Exception as e:
            print(f"无法打开日志文件: {e}")
    else:
        print("没有找到最新的日志文件")

def stop_playback(icon, item):
    """停止当前播放"""
    global is_playing, exit_flag
    is_playing = False
    exit_flag = True
    print("已停止播放")

def create_tray_icon():
    """创建托盘图标"""
    try:
        # 使用相对路径加载图标
        icon_path = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'icon', 'bell.ico')
        image = Image.open(icon_path)
        
        # 创建菜单
        menu = (
            pystray.MenuItem('打开日志', open_latest_log),
            pystray.Menu.SEPARATOR,
            pystray.MenuItem('结束当前播放', stop_playback),
        )
        
        # 创建图标
        icon = pystray.Icon('BellPlayer', image, '铃声播放器', menu)
        
        # 启动图标线程
        icon_thread = threading.Thread(target=icon.run, daemon=True)
        icon_thread.start()
        return icon
    except Exception as e:
        print(f"创建托盘图标失败: {e}")
        return None

def play_audio_with_progress(file_path, volume=1.0):
    """播放音频文件并显示进度条"""
    global is_playing, current_log_path, exit_flag
    is_playing = True
    logger, info_log = setup_logging()
    current_log_path = info_log

    logger.info(f"play_audio_with_progress 函数被调用: 文件={file_path}, 音量={volume}")
    logger.info(f"当前工作目录: {os.getcwd()}")
    logger.info(f"Python可执行文件路径: {sys.executable}")

    try:
        logger.info("=" * 50)
        logger.info(f"程序启动: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
        logger.info(f"INFO日志文件: {info_log}")
        
        # 标准化文件路径（处理混合斜杠问题）
        file_path = os.path.normpath(file_path)
        logger.info(f"标准化文件路径: {file_path}")
        
        # 验证音频文件
        validate_audio_file(file_path)
        logger.info(f"音频文件验证成功: {file_path}")
        
        # 尝试读取文件信息（不加载全部数据）
        try:
            file_info = sf.info(file_path)
            logger.info(f"音频文件信息: 格式={file_info.format}, 采样率={file_info.samplerate}, 通道数={file_info.channels}, 时长={file_info.duration:.2f}秒")
        except Exception as e:
            logger.warning(f"无法获取音频文件详细信息: {e}")
        
        # 检查音频设备
        device_info = check_audio_device()
        logger.info(f"将使用输出设备: {device_info['name']} (ID: {sd.default.device[1]})")
        
        # 读取音频文件并应用音量增益
        try:
            # 尝试自动检测格式读取
            data, fs = sf.read(file_path, dtype='float32')
            data *= volume  # 应用音量增益
            logger.info(f"音频文件加载成功: {file_path}")
            logger.info(f"采样率: {fs}Hz, 时长: {len(data)/fs:.2f}秒")
            logger.info(f"通道数: {data.shape[1] if len(data.shape) > 1 else 1}")
        except sf.LibsndfileError as e:
            logger.warning(f"自动检测格式失败: {e}，尝试使用WAV格式参数读取...")
            # 尝试使用WAV格式参数读取
            try:
                # 尝试使用WAV格式读取，但不强制采样率和通道数
                # 只指定format为WAV，让libsndfile尝试自动检测其他参数
                data, fs = sf.read(file_path, dtype='float32', format='WAV')
                data *= volume  # 应用音量增益
                logger.info(f"使用WAV格式读取成功: {file_path}")
                logger.info(f"采样率: {fs}Hz, 时长: {len(data)/fs:.2f}秒")
                logger.info(f"通道数: {data.shape[1] if len(data.shape) > 1 else 1}")
            except Exception as e2:
                logger.warning(f"WAV格式读取失败: {e2}，尝试使用宽松参数读取...")
                # 尝试使用更宽松的参数设置
                try:
                    data, fs = sf.read(file_path, dtype='float32', format='WAV', samplerate=0, channels=0)
                    data *= volume  # 应用音量增益
                    logger.info(f"使用宽松参数读取WAV成功: {file_path}")
                    logger.info(f"采样率: {fs}Hz, 时长: {len(data)/fs:.2f}秒")
                    logger.info(f"通道数: {data.shape[1] if len(data.shape) > 1 else 1}")
                except Exception as e3:
                    create_level_log(logger, logging.ERROR, f"无法读取音频文件，请检查文件完整性: {e3}", traceback.format_exc())
                    raise
        
        # 打印设备详细信息
        logger.info("输出设备详细信息：")
        for key, value in device_info.items():
            logger.info(f"  {key}: {value}")
        
        # 计算音频总时长（秒）
        total_duration = len(data) / fs
        
        # 创建数据副本用于播放
        play_data = data.copy()
        
        # 跟踪实际播放的帧数
        played_frames = 0

        # 进度条总长度
        BAR_LENGTH = 50

        # 添加播放状态标志
        is_playing = True
        is_finished = False
        
        # 添加线程锁避免竞争条件
        status_lock = threading.Lock()

        # 定义回调函数，用于在播放时更新信息（仅控制台显示，不记录日志）
        def callback(outdata, frames, time_info, status):  # pyright: ignore[reportUnusedParameter]
            global is_playing
            nonlocal play_data, played_frames, is_finished
            
            # 检查退出标志
            if exit_flag:
                is_playing = False
                raise sd.CallbackStop
            
            with status_lock:
                if is_finished:
                    return
                
                if status:
                    create_level_log(logger, logging.WARNING, f"状态: {status}")
                
                current_time = played_frames / fs
                remaining_time = max(0, total_duration - current_time)
                progress = min(100, current_time / total_duration * 100)
                filled_length = int(BAR_LENGTH * progress / 100)
                bar = '#' * filled_length + '-' * (BAR_LENGTH - filled_length)
                current_volume = max(abs(outdata.flatten())) if len(outdata) > 0 else 0
                
                # 仅控制台显示，不记录日志
                print(f"\r{GREEN}[{bar}]{END} {progress:.2f}% 时间: {current_time:.2f}/{total_duration:.2f}秒 剩余: {remaining_time:.2f}秒 音量: {current_volume:.2f}", end='', flush=True)
                
                chunk = play_data[:frames]
                if len(chunk) < frames:
                    outdata[:len(chunk)] = chunk.reshape(-1, 1) if len(chunk.shape) == 1 else chunk
                    outdata[len(chunk):].fill(0)
                    raise sd.CallbackStop
                else:
                    outdata[:] = chunk.reshape(-1, 1) if len(chunk.shape) == 1 else chunk
                
                played_frames += len(chunk)
                play_data = play_data[frames:]
        
        # 定义播放完成回调函数（记录最终状态）
        def finished_callback():
            global is_playing
            nonlocal is_finished
            with status_lock:
                if not is_finished:
                    is_playing = False
                    is_finished = True
                    
                    # 计算最终状态
                    current_time = played_frames / fs
                    progress = min(100, current_time / total_duration * 100)
                    filled_length = int(BAR_LENGTH * progress / 100)
                    bar = '#' * filled_length + '-' * (BAR_LENGTH - filled_length)
                    remaining_time = max(0, total_duration - current_time)
                    current_volume = max(abs(data[:played_frames].flatten())) if played_frames > 0 else 0
                    
                    # 记录最终进度条到日志
                    final_status = f"{GREEN}[{bar}]{END} {progress:.2f}% 时间: {current_time:.2f}/{total_duration:.2f}秒 剩余: {remaining_time:.2f}秒 音量: {current_volume:.2f}"
                    logger.info(final_status.replace('\r', ''))  # 移除回车符
                    
                    logger.info("播放完成!")
                    print()
                    print("\n播放完成!\n")


        
        # 注册退出处理函数
        def exit_handler():
            global is_playing
            is_playing = False
            logger.info("程序正常退出")
        
        atexit.register(exit_handler)

        # 创建并启动输出流
        with sd.OutputStream(
            device=sd.default.device[1],
            samplerate=fs,
            channels=data.shape[1] if len(data.shape) > 1 else 1,
            callback=callback,
            finished_callback=finished_callback
        ) as stream:
            logger.info("开始播放音频...")
            print("\n开始播放音频...\n")
            
            while is_playing and not exit_flag:
        
                time.sleep(0.1)
                
            if not is_playing or exit_flag:
                stream.stop()
                logger.info("播放已停止")
                print("\n播放已停止\n")
    
    except FileNotFoundError as e:
        trace = None
        create_level_log(logger, logging.ERROR, f"文件错误: {e}", trace)
    except PermissionError as e:
        create_level_log(logger, logging.ERROR, f"权限错误: {e}", None)
    except ValueError as e:
        create_level_log(logger, logging.ERROR, f"参数错误: {e}", None)
    except sf.LibsndfileError as e:
        create_level_log(logger, logging.ERROR, f"音频文件错误: {e}", None)
    except sd.PortAudioError as e:
        create_level_log(logger, logging.ERROR, f"音频设备错误: {e}", None)
    except Exception as e:
        trace = traceback.format_exc()
        create_level_log(logger, logging.ERROR, f"未知错误: {e}", trace)
    finally:
        logger.info(f"程序结束: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
        logger.info("=" * 50)
        
        for handler in logging.getLogger().handlers[:]:
            logging.getLogger().removeHandler(handler)
        sys.stdout.flush()

def parse_arguments():
    """解析命令行参数并处理错误"""
    parser = argparse.ArgumentParser(description='播放音频文件')
    parser.add_argument('-p', '--path', default='./', help='文件母路径，默认当前目录')
    parser.add_argument('-f', '--file', required=True, help='要播放的文件名')
    parser.add_argument('-v', '--volume', type=float, default=1.0, help='音量增益，默认1.0')
    
    try:
        args = parser.parse_args()
        return args
    except SystemExit as e:
        logger, _ = setup_logging()
        create_level_log(logger, logging.WARNING, f"参数解析错误: {sys.argv}")
        for handler in logging.getLogger().handlers[:]:
            logging.getLogger().removeHandler(handler)
        sys.exit(e.code)

if __name__ == "__main__":
    # 注册信号处理函数
    signal.signal(signal.SIGTERM, signal_handler)
    signal.signal(signal.SIGINT, signal_handler)
    
    # 创建托盘图标
    tray_icon = create_tray_icon()
    
    args = parse_arguments()
    audio_file = os.path.join(args.path, args.file)
    
    # 使用线程播放音频，避免阻塞托盘图标
    play_thread = threading.Thread(target=play_audio_with_progress, args=(audio_file, args.volume))
    play_thread.start()
    
    # 等待播放线程完成
    try:
        logger.info(f"等待播放线程完成")
        play_thread.join()
        logger.info(f"播放线程已完成")
    except KeyboardInterrupt:
        logger.info(f"播放线程被用户中断")
        # 播放线程会在 KeyboardInterrupt 异常处理后自动退出
        logger.info(f"播放线程已完成")
    
    # 如果托盘图标存在，停止它
    if tray_icon:
        tray_icon.stop()